iT邦幫忙

0

C 語言-從放棄到入門 CH2 : 資料型態

c
  • 分享至 

  • xImage
  •  

如果你是學高階語言(例如python),我們就可以結束這課教學,很可惜C語言的底層性質讓這一切的開始沒這麼簡單...orz

我們可以先有些mindset,進到C這門較底層的語言,記憶體對於程式設計師是裸露的;資料型態只是對記憶體區段賦予意義,協助程式了解該區段記憶體儲存的值應該要如何解釋;從這個角度一方面能理解資料型態間的轉換是多麼容易出錯(因為涉及對記憶體區段的重新解釋(re-intepretation)),另一方面也理解資料型態本身在這層意義上是抽象的例如不同的機器用多少位元儲存"同樣的"資料型態,也將有差異;所幸現在已經有一些方法讓我們初學時不必太在意這些問題。

先從C語言提供的資料型態來看,雖然種類較為繁多、靈活,且難以掌握,但目前不避太過拘泥於細節;粗略有三種資料型態:

  1. 數字類的(int, float, double)
  2. 文字類的(char, wchar_t)
  3. 布林值bool(邏輯真假值);簡言之,數字只要記得整數(int)和小數(double)、文字記得單一字元(char)和布林值即可。

void 不是資料型態,而是如其名代表空 (no data),常用於聲明函數沒有返還值;一個比較貼近資料型態的概念是,void* ptr代表聲明一個變數ptr用於儲存指向某個記憶體位址,而且指向的位址沒有指定是用於儲存哪種資料型態的值。

基於數字類的資料型態,我們先提供一個便捷的思考方式 : 如果儲存的值只可能是整數,不管正負數皆使用long(至少32bits);儲存的值有可能小數則使用 double;不過緊接著,我們就會介紹比較正統的思考方法...

不要因為它只用於儲存正整數就很 hack的想用uint,或計算 float 儲存值得範圍,通常double夠用了,再大部分的機器上也不會造成太嚴重的效能影響;至少初學時我們不必關注這些細節。


關於資料型態,以下介紹兩個大魔王級的概念: 1.資料型態的位元數和 2.字元的混亂型態:

1. 資料型態的位元數

關於資料型態的位元數,癥結點在於C提供了貼近機器的靈活設計,使得每個資料型態可以儲存多大的內容來表示數值有些具體的規範;例如int在某些機器上"最小"只能儲存16位元(bit),因為int預設是有號整數,這代表我們要分一半儲存負數(2^15)、另一半儲存正數(2^15-1),一個值給不屬於正數或負數的0,技術上而言2^15+(2^15-1)+1=2^16,剛好展示16bit能儲存的範圍;因此儲存的值從-32768(-2^15)至32767(2^15-1),超過就會rollback回來,甚麼是rollback不重要,總之我們總需避免而根據機器指定一個夠大的儲存位元;好消息是今年已是2024年,即使是C語言的使用者,我們也不是野蠻人硬測這台機器是16bit或32bit,交給標準函數庫吧。include<stdint.h>,處理標準定義的函數庫允許我們顯示的指定位元數,以解決跨平台的需求;例如整數有int8_t, int16_t, ..., int64_t,要幾位元就聲明幾位元,若機器不支援它也會告訴你的。

2. 字元的混亂型態

關於字元(char),最初它只能表示一個包含於 字元集(character set) 的字元,有點繞口..;讓我們看個例子,char c = 'a';,很好,這樣ok!

讓我們看一些不ok的例子,char c = 'abc';,它會給你一個警告(warning: overflow in conversion from ‘int’ to ‘char’),因為你正在轉換multi-char這個 多字元 整數至單一字元char,而它會引發overflow,因為char如我們所知只能儲存"一個"字元集中的字元;multi-char是個血脈上與char相近的型態,只要知道你用單引號框住的字元都是在聲明這個資料型態,而它是一個整數型態,例如'abc',不過讓我們先忘了它,只要記得我們不應在單引號內放置多於一個字元,否則就是在聲明multi-char

ok,理解濫用單引號的下場和知道char的極限後,發現我們只學了一個毫不實用的方法來定義文字的變數,因為它只能儲存一個字元;這個問題可以被陣列(array)解決,陣列是一次定義多個同一資料型態的變數,並且它會自動分派、釋放儲存著多個值的記憶體(等到介紹這個可怕的故事已經是好幾章之後的事了);例如 int arr[3] = {0, 1, 2};,一次我們就定義了儲存三個整數的變數arr,取得整數二也很直覺arr[2],index為2是因為index從0起算。

如果你是高手精通指標,你可能會嘗試將範例改成char* ch_ptr = "abd";,這樣的舉一反三很好,但與陣列相比,我建議你可以併用strdup(string duplication)來複製字串,以免這個非const的字串有更改其中內容的需求,由於指標是指向常量,而C語言對於如何儲存字串常量是未定義的,你可能會成功或失敗;因此建議修改前先複製一份 (char* ch_arr = strdup("abd");),這樣好多了,之後就可以避免 ch_arr[0] = 'B';產生出乎意料的結果。

因此,我們同樣可用char ch_arr[3] = {'a', 'b', 'c'};來儲存三個字元.. 我的天,數字還可以,文字這樣定義簡直要人命;前面我們提過單引號內放入多於一個字元不行,那放入雙引號可不可以?
char ch_arr[3] = "abc"
好消息是這樣看似可行,但為何可行又是另一個長篇故事了;簡言之,雙括弧內放置的多個字元"abc"會被解譯成字串(c style string, 簡稱c_string)常量,是字串而不是字元(char),因此等號間其實通過一個資料型態的轉換,將字串轉成字元陣列;但資料型態的轉換不是第一課要說明的,我們先理解要放多個字元就請用雙括弧吧!

最後,為了更深入的使用printf函數來理解我們定義的變數,我們稍微深入介紹一下printf。
先下一段函數的聲明:
printf( "formatted_string", arguments_list);,formatted_string是一個包含指定型態的字串,指定的型態就是我們之前提及的 int, double, char ... 等等;在指定時他們也有對應的英文字母 int(%d, digit)、char(%c, char), c_string(%s, string),依此類推,請你查詢其他資料型態的英文字母;而arguments_list則按照順序存放各變數值。


讓我們下一段程式,統整一下上述的概念:

#include<stdio.h>

int main(){
    int num = 1;
    char c_str1[4] = "abc";
    char no_stop_cstr[3] = "abc";
    /* it's not necessary place '\0' in {}, but we do it explicitly
     to show that how char array (c_string) declare the stop symbol! */
    char c_str2[4] = {'d', 'e', 'f', '\0'};

    printf("c lang lesson %d\n", num);
    printf("char value : %c\n", c_str1[1]);
    printf("c_string value : %s\n", c_str1);
    printf("raw c_string value : %s\n", c_str2);
    printf("so far so good..\n------------\n");
    printf("boom! : %s, i can not stop to print out...orz", no_stop_cstr);
    return 0;
}

執行程式

# 偷偷介紹一個旗標(flag),-Wall (warning all message) 會開啟所有編譯警告
# 如果 gcc 發現你的程式碼有甚麼奇怪的地方,它將會毫不吝嗇地告訴你
!gcc -o test.out -Wall test.c ; ./test.out

# 輸出
c lang lesson 1
char value : b
c_string value : abc
raw c_string value : def
so far so good..
------------
boom! : abcabc, i can not stop to print out...orz

好的,從程式的執行結果,看來我們忽略了一點關於c_string的說明,事實上我們並沒有告訴printf什麼時候要停,c_string是以'\0'這個字符作為聲明字串的尾端;上面初始化的例子中我們直接用雙引號聲明了一個字串(c_string中文我就直稱字串了),而我們聲明的char array會在尾端自帶'\0',因此我們不必特別聲明,不過我們卻需要記得比輸入的字串多聲明一個記憶體的位址,如此'\0'才不會被char值覆寫,以至於產生上述no_stop_cstr的例外,它一路輸出了其他變數的值,因為他們的記憶體位址恰巧被連續分配在同一區塊,否則就不是這麼幸運,而會因非法入侵他人領土,引發一個著名的segment fault錯誤

然而,工程師不能每次都期望恰巧的發現這個bug,或防患未然的多聲明一個位址空間;問題出在我們的字串常量(c_string literal)應該提供了足夠的資訊,例如至\0之前包含幾個字元,但我們仍手動聲明要預先分配多少字元的空間,事實上定義array的方式可以更加聰明,如果你提供了足夠的資訊(例如各式常量)。
我們單獨修正聲明 array 的地方,刻意不提供分配多少空間,讓編譯器推論 ~

#include<stdio.h>

// 被我們發現C語言一些酷炫的東西了,之後再介紹巨集 ~
// sizeof "關鍵字" 回傳分配的記憶體量, 而此巨集用來計算陣列的長度,不過也有失靈的時候..orz
#define len(arr) sizeof(arr)/sizeof(arr[0])

int main(){
    char c_str[] = "abc";
    // google ascii code, 作為最後一個範例,內容是"BYE BYE" 
    int arr[] = {66, 89, 69, 32, 66, 89, 69};
    printf("raw c_string value : %s\n", c_str);
    
    // %lu, 印出 unsigned long數值,len 巨集回傳陣列長度
    // 可以發現它會 "按照常量" 分配4個字元給 c_str,包括 '\0' 
    printf("allocated space : %lu\n", len(c_str));
    
    // 一個偷懶將整數陣列印出字元群的做法
    for(int idx=0; idx<len(arr);++idx)
        printf("%c", arr[idx]);
}

程式執行結果

raw c_string value : abc
allocated space : 4
BYE BYE

可以看到,其他資料型態的陣列可以支援這種自動分派記憶體的方式,但要如何取得陣列的長度又是另一個問題了...

這邊我們先用一個簡單的巨集解決了,巨集這個強大但危險的功能會再後續慢慢介紹,如何在力量與危險兩者間取得平衡,將是一個很展現C語言程式設計師能力的問題 ~

預告 CH3 : 資料型態的轉換


update log : 2024/11/24 19:12,

  1. 略述從記憶體觀點看資料型態的影響
  2. 微調用詞、語句
  3. 補充陣列說明
  4. 補上最後範例程式的執行結果

圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言